package de.athedevel;

import java.util.Map;
import java.util.TreeMap;

/**
 * Command Line Interface without any external libraries
 * 
 * @version 1.00
 * @release 23.01.2014
 */
public class Cli
{

    public static class Parameter implements Comparable<Parameter>
    {

        String key;
        String value;
        String description;
        boolean required = false;
        String defaultValue = null;
        Class<?> type;

        /**
         * Instanziert den Parameter
         * 
         * @param key der Name des Parameters,
         * @param type Typ des Parameters als Class. z.B. {@link String}
         */
        public Parameter(String key, Class<?> type)
        {
            if (!(type.equals(String.class) || type.equals(Boolean.class) || type.equals(Integer.class)))
            {
                throw new IllegalArgumentException(String.format("type '%s' nicht unterstuetzt", type));
            }

            this.key = key;
            this.value = null;
            this.type = type;
        }

        public Parameter setDefaultValue(String defaultValue)
        {
            if (this.required)
            {
                throw new IllegalArgumentException("ein Feld, das required ist, kann keinen default Wert haben.");
            }
            this.defaultValue = defaultValue;
            return Parameter.this;
        }

        public Parameter setRequired(boolean r)
        {
            if (this.defaultValue != null)
            {
                throw new RuntimeException("Ein Wert mit einem default Value darf nicht auf required gesetzt werden");
            }
            this.required = r;
            return Parameter.this;
        }

        /**
         * setzt den Wert des Parameters
         * 
         * @param v Wert
         */
        public void setValue(String v)
        {
            this.value = v;
        }

        public Parameter setDescription(String s)
        {
            this.description = s;
            return Parameter.this;
        }

        public String getDescription()
        {
            return this.description;
        }

        public String getKey()
        {
            return this.key != null ? this.key : "";
        }

        public boolean hasValue()
        {
            return this.value != null || this.defaultValue != null;
        }

        public String getValue()
        {
            if (this.value != null)
            {
                return this.value;
            }
            if (this.defaultValue != null)
            {
                return this.defaultValue;
            }
            return "";
        }

        public Class<?> getType()
        {
            return this.type != null ? this.type : null;
        }

        /*
         * (non-Javadoc)
         * @see java.lang.Comparable#compareTo(java.lang.Object)
         */
        @Override
        public int compareTo(Parameter o)
        {
            return this.key.compareTo(o.key);
        }

        public String toString()
        {
            return String.format("%s (%s): '%s'", this.getKey(), this.getType() != null ? this.getType().getSimpleName() : "null", this.getValue());
        }
    }

    /**
     * ParameterDefinitions,
     * <dl>
     * <dt>key:</dt>
     * <dd>parametername. Beispiel: 's' wird realisiert als '-s'</dd>
     * <dt>value:</dt>
     * <dd>Instanz von {@link Parameter}</dd>
     * </dl>
     */
    Map<String, Parameter> pds = null;

    /**
     * die aufrufende Klasse
     */
    String caller = null;

    /**
     * Beschreibung des Programms
     */
    private String description;

    public Cli(String caller)
    {
        this.caller = caller;
        this.pds = new TreeMap<String, Cli.Parameter>();
        this.addParameter(new Parameter("h", Boolean.class).setDescription("gibt diese Hilfe aus"));
    }

    public Cli addParameter(Parameter p)
    {
        if (this.pds.containsKey(p.getKey()))
        {
            throw new IllegalStateException(String.format("parameter '%s' bereits definiert", p.getKey()));
        }
        this.pds.put(p.getKey(), p);
        return this;
    }

    public String helpText()
    {
        StringBuilder sb = new StringBuilder();

        sb.append("\n");
        sb.append(this.description);
        sb.append("\n\n");
        sb.append("Usage:\n");
        sb.append(String.format("java %s <parameterlist>\n", this.caller));
        sb.append("\n");
        sb.append("<parameterlist>:\n");
        for (Parameter p : pds.values())
        {

            if (p.type.equals(String.class) || p.type.equals(Integer.class))
            {
                sb.append(String.format("-%s <value> (%s%s)\n", p.key, p.required ? "erforderlich" : "", p.defaultValue != null ? p.defaultValue : ""));
                sb.append(" ").append(p.getDescription());
                sb.append("\n");
            }
            if (p.type.equals(Boolean.class))
            {
                sb.append(String.format("-%s\n", p.key));
                sb.append(" ").append(p.getDescription());
                sb.append("\n");

            }
        }
        sb.append("\n");
        return sb.toString();
        // return "Usage: ParseCmdLine [-verbose] [-xn] [-output afile] filename";
    }

    public Cli setDescription(String s)
    {
        this.description = s;
        return this;
    }

    public void parse(String[] args)
    {

        int i = 0;
        String arg;
        int nonFlagArguments = 0;

        while (i < args.length)
        {
            arg = args[i++];

            // use this type of check for "wordy" arguments
            if (arg.startsWith("-") && pds.containsKey(arg.substring(1)))
            {
                Parameter p = pds.get(arg.substring(1));
                if (p.type.equals(Boolean.class))
                {
                    p.setValue("true");
                }
                else if (p.type.equals(String.class))
                {
                    if (i < args.length)
                        p.setValue(args[i++]);
                    else
                        System.err.println(arg.substring(1) + " requires an argument");
                }
                else if (p.type.equals(Integer.class))
                {
                    if (i < args.length)
                        p.setValue("" + Integer.parseInt(args[i++]));
                    else
                        System.err.println(arg.substring(1) + " requires an argument");
                }
                else
                {
                    throw new IllegalStateException(String.format("type '%s' nicht bekannt", p.type));
                }
            }

            // use this type of check for a series of flag arguments
            else
            {
                Parameter p = new Parameter("" + nonFlagArguments, String.class);
                p.setValue(arg);
                pds.put("" + nonFlagArguments, p);
            }
        }
        if (this.hasRequiredParameterUnset() || this.getParameter("h").getValue().equals("true"))
        {
            System.err.println(helpText());
            System.exit(1);
        }

    }

    /**
     * @return true, wenn erforderliche Parameter ohne Wert sind
     */
    private boolean hasRequiredParameterUnset()
    {
        for (Parameter p : pds.values())
        {
            if (p.required && !p.hasValue())
                return true;
        }
        return false;
    }

    public String toString()
    {
        StringBuffer sb = new StringBuffer();
        for (Parameter p : this.pds.values())
        {
            sb.append(p.toString());
            sb.append("; ");
        }
        return sb.toString();
    }

    /**
     * @param key
     * @return zugehoeriger Parameter
     */
    public Parameter getParameter(String key)
    {
        return this.pds.get(key);
    }
}
